/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package dev.colin;

import dev.colin.SessionEvent;
import dev.colin.SessionServer;
import dev.colin.SocketUtils;
import lib.colin.DataAccess;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.UnsupportedEncodingException;
import java.net.SocketAddress;
import java.util.Date;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JFrame;
import javax.swing.table.TableColumn;
import lib.colin.ServerTableModel;
import lib.colin.StringUtils;

/**
 *
 * @author colin
 */
public class SimpleServer extends JFrame implements SessionEvent {

    /**
     * Creates new form SimpleServer
     */
    public SimpleServer() {
        initComponents();
        b_ui_occupied   = false;
    }
    
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        // TODO code application logic here
        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable(){
            @Override
            public void run(){
                new SimpleServer().setVisible(true);
            }
        });
    }
    
    @Override
    public synchronized void session_connect_event(SocketAddress remote_address)
    {
        try
        {
            if (true == b_ui_occupied)
            {
                wait();
            }
            b_ui_occupied = true; 
            System.out.println("socket was connected!");
            // TODO: Add the codes for socket connected event here.

            // Firstly, if we find the IP address and port in the table, then we
            // remove it first.
            remove_client_info(remote_address);
            
            // Secondly, add the address and port of the new connection in the table.
            add_client_info(remote_address);
            
            // thirdly, Update the UI of the table.
            node_info_table.updateUI();
            b_ui_occupied = false;
            notify();
        }
        
        catch(InterruptedException ex)
        {
            System.out.println(ex.toString());
        }
    }

    @Override
    public synchronized void session_close_event(SocketAddress remote_address)
    {
        try
        {
            if (true == b_ui_occupied)
            {
                wait();
            }
            b_ui_occupied = true; 
            System.out.println("socket "
                    + SocketUtils.socket_address_2_string(remote_address)
                    + " was closed!");
            // TODO: Add the codes for socket clsoed event here.
            if(true == remove_client_info(remote_address))
            {
                this.node_info_table.updateUI();
            }
            
            b_ui_occupied = false;
            notify();
        }
        
        catch(InterruptedException ex)
        {
            System.out.println(ex.toString());
        }
    }
    
    @Override
    public synchronized void session_read_event(SocketAddress remote_address, byte[] data, int length)
    {
        try
        {
            int row;    /*The row in the table who received data this time.*/
            
            if (true == b_ui_occupied)
            {
                wait();
            }
            b_ui_occupied = true; 
            System.out.println("receiving data!" + length);
            // TODO: Add the codes for receiving data here.
            row  = get_row_by_address(remote_address);
            
            if (row != -1)
            {
                /**
                 * Save the raw data first.
                 */
                DataAccess.save_raw_data(
                        (String)(this.client_table_model.getSessionID(row)) + "(R)",
                        data, length);

                
                /** if the function parse_received_data returns true, it means we
                 * got a complete message successfully.
                 */
                this.lb_source_addr.setText(
                        SocketUtils.socket_address_2_string(remote_address));
                
                String display_data;
                if (this.hex_recv.isSelected())
                {
                    display_data = StringUtils.bytes2HexString(data, length);
                }
                else
                {
                    display_data = new String(data, 0, length, "US-ASCII");
                }
                this.ta_recv.setText(display_data);

                update_access_time(row);
                
            }
            
            b_ui_occupied = false;
            notify();
        }
        
        catch(InterruptedException | UnsupportedEncodingException ex)
        {
            System.out.println(ex.toString() + "in SimpleServer");
        }
    }
    
    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        labelServer = new javax.swing.JLabel();
        labelPort = new javax.swing.JLabel();
        server_type_list = new javax.swing.JComboBox<>();
        tf_port = new javax.swing.JTextField();
        bt_delete = new javax.swing.JButton();
        bt_start = new javax.swing.JButton();
        jScrollPane1 = new javax.swing.JScrollPane();
        node_info_table = new javax.swing.JTable();
        bt_stop = new javax.swing.JButton();
        lb_output = new javax.swing.JLabel();
        lb_dest_addr = new javax.swing.JLabel();
        bt_send = new javax.swing.JButton();
        lb_input = new javax.swing.JLabel();
        lb_source_addr = new javax.swing.JLabel();
        ta_send = new javax.swing.JTextArea();
        ta_recv = new javax.swing.JTextArea();
        jscp_send = new javax.swing.JScrollPane();
        jscp_recv = new javax.swing.JScrollPane();
        client_table_model = new ServerTableModel(new String[]{
            SimpleServerR.string.session_id_name, 
            SimpleServerR.string.source_addr_name,
            SimpleServerR.string.access_time_name
        }, 0);
        
        hex_recv = new javax.swing.JCheckBox(SimpleServerR.string.hex_check_name);
        hex_send = new javax.swing.JCheckBox(SimpleServerR.string.hex_check_name);

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        setTitle(SimpleServerR.string.frame_title_name
                + " " + this.main_version + this.build_version);

        labelServer.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        labelServer.setText(SimpleServerR.string.server_type_name);

        labelPort.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        labelPort.setText(SimpleServerR.string.port_label_name);

        server_type_list.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        server_type_list.setModel(new DefaultComboBoxModel<>(new String[] {SocketUtils.TCP_TYPE_NAME, SocketUtils.UDP_TYPE_NAME}));

        tf_port.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        tf_port.setText("1024");

        bt_start.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        bt_start.setText(SimpleServerR.string.switch_on_name);
        bt_start.setActionCommand("Start");
        bt_start.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                bt_startActionPerformed(evt);
            }
        });

        bt_delete.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        bt_delete.setText(SimpleServerR.string.bt_delete_name);
        bt_delete.setActionCommand("Delete");
        bt_delete.addActionListener(new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                bt_DeleteActionPerformed(evt);
            }
        });

        node_info_table.setFont(new java.awt.Font("Times New Roman", 0, 12)); // NOI18N
        node_info_table.setModel(this.client_table_model);
        node_info_table.setGridColor(new java.awt.Color(255, 255, 255));
        TableColumn session_column = node_info_table.getColumn(
                SimpleServerR.string.session_id_name);
        session_column.setPreferredWidth(65);
        session_column.setMaxWidth(150);
        session_column.setMinWidth(50);
        jScrollPane1.setViewportView(node_info_table);
        node_info_table.addKeyListener(new KeyListener(){
            @Override
            public void keyTyped(KeyEvent e) {
            }

            @Override
            public void keyPressed(KeyEvent e) {
                node_info_keyevent_pressed(e);
            }

            @Override
            public void keyReleased(KeyEvent e) {
            }
        });

        bt_stop.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        bt_stop.setText(SimpleServerR.string.switch_off_name);
        bt_stop.setToolTipText("");
        bt_stop.setActionCommand("Stop");
        bt_stop.setEnabled(false);
        bt_stop.addActionListener(new java.awt.event.ActionListener(){
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                bt_stopActionPerformed(evt);
            }
        });

        lb_output.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        lb_output.setText(SimpleServerR.string.send_text_title);

        lb_dest_addr.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        lb_dest_addr.setText(SimpleServerR.string.no_device_inform);

        bt_send.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        bt_send.setText(SimpleServerR.string.bt_send_name);
        bt_send.setToolTipText("");
        bt_send.setActionCommand("Send");
        bt_send.addActionListener(new java.awt.event.ActionListener(){
            @Override
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                bt_SendActionPerformed(evt);
            }
        });

        lb_input.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        lb_input.setText(SimpleServerR.string.recv_text_title);

        lb_source_addr.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N

        ta_send.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        ta_send.setCursor(new java.awt.Cursor(java.awt.Cursor.TEXT_CURSOR));
        ta_send.setLineWrap(true);
        jscp_send.setViewportView(ta_send);

        ta_recv.setFont(new java.awt.Font("Times New Roman", 0, 14)); // NOI18N
        ta_recv.setCursor(new java.awt.Cursor(java.awt.Cursor.TEXT_CURSOR));
        ta_recv.setLineWrap(true);
        jscp_recv.setViewportView(ta_recv);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGap(30, 30, 30)
                .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 330, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 30, Short.MAX_VALUE)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                        .addComponent(bt_delete, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(bt_start, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(bt_stop, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE))
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(lb_output)
                                .addGap(10, 10, 10)
                                .addComponent(lb_dest_addr, javax.swing.GroupLayout.PREFERRED_SIZE, 120, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addComponent(hex_send)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addComponent(bt_send))
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(lb_input)
                                .addGap(10, 10, 10)
                                .addComponent(lb_source_addr)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addComponent(hex_recv))
                            .addGroup(layout.createSequentialGroup()
                                .addComponent(labelServer)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(server_type_list, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addComponent(labelPort, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(tf_port, javax.swing.GroupLayout.PREFERRED_SIZE, 44, javax.swing.GroupLayout.PREFERRED_SIZE)
                                )))
                    .addComponent(jscp_send)
                    .addComponent(jscp_recv))
                .addGap(44, 44, 44))
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGap(30, 30, 30)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                    .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 430, javax.swing.GroupLayout.DEFAULT_SIZE)
                    .addGroup(layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                .addComponent(tf_port, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addComponent(labelPort))
                            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                                .addComponent(labelServer, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addComponent(server_type_list, javax.swing.GroupLayout.DEFAULT_SIZE, 25, Short.MAX_VALUE)))
                        .addGap(18, 18, 18)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(bt_delete, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(bt_start, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(bt_stop, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addGap(24, 24, 24)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(lb_output)
                            .addComponent(lb_dest_addr)
                            .addComponent(hex_send)
                            .addComponent(bt_send, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addGap(18, 18, 18)
                        .addComponent(jscp_send, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.DEFAULT_SIZE)
                        .addGap(18, 18, 18)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(lb_input)
                            .addComponent(lb_source_addr)
                            .addComponent(hex_recv, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addGap(18, 18, 18)
                        .addComponent(jscp_recv, javax.swing.GroupLayout.PREFERRED_SIZE, 125, javax.swing.GroupLayout.DEFAULT_SIZE)))
                .addGap(44, 44, 44))
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void bt_startActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_startActionPerformed
        // TODO add your handling code here:
        try
        {
            int server_port = Integer.parseInt(tf_port.getText());
                    
            /*Succesfully to save server configuration. Now we
            can create the main interface and start the server
            listener.*/
            /*Now we should disable start button and enable stop button*/
            bt_start.setEnabled(false);
            bt_stop.setEnabled(true);

            /**
             * Do not allow to change server type and port number now
             * after the server is started.
             */
            server_type_list.setEnabled(false);
            tf_port.setEditable(false);

            /*Now we should start the server.*/
            server_socket = new SessionServer(
                    (String)server_type_list.getSelectedItem(), server_port, this);
            server_socket.start_listen();
        }
                
        catch (NumberFormatException ex)
        {
            System.out.println(ex.toString());
        }
         
    }//GEN-LAST:event_startActionPerformed

    private void bt_stopActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bt_stopActionPerformed
        // TODO add your handling code here:
        /*Now we enable the start button and disable the stop button*/
        bt_stop.setEnabled(false);
        bt_start.setEnabled(true);
        
        /**
         * After the server is stopped, we can edit server type and port now.
         */
        server_type_list.setEnabled(true);
        tf_port.setEditable(true); 
        
        // Stop the server socket, so we close the socket first.
        server_socket.close_server();
        server_socket = null;
    }//GEN-LAST:event_bt_stopActionPerformed
    
    private void bt_SendActionPerformed(java.awt.event.ActionEvent evt)
    {
        SocketAddress  remote_address;
        int[]   row;
        
        if (this.server_socket != null)
        {
            row = this.node_info_table.getSelectedRows();
            if (row.length > 0)
            {
                remote_address = (SocketAddress)(this.client_table_model.getSocketAddress(row[0]));
                
                String str_data = this.ta_send.getText();
                
                if (str_data.length() > 0)
                {
                    if (this.hex_send.isSelected())
                    {
                        byte[] data = StringUtils.hexString2Bytes(str_data);
                        this.server_socket.socket_write_data(remote_address, data, data.length);

                        DataAccess.save_raw_data(((String)this.client_table_model.getSessionID(row[0]) + "(WH)"), str_data);
                    }
                    else
                    {
                        this.server_socket.socket_write_data(remote_address, str_data);

                        DataAccess.save_raw_data(((String)this.client_table_model.getSessionID(row[0]) + "(WA)"), str_data);
                    }

                    this.lb_dest_addr.setText(
                            SocketUtils.socket_address_2_string(remote_address));
                    System.out.println("Send Data " + str_data);                        
                }
            }
        }
    }
    
    private void bt_DeleteActionPerformed(ActionEvent evt)
    {
        remove_selected_node();
    }
    
    private void node_info_keyevent_pressed(java.awt.event.KeyEvent evt)
    {
        if (java.awt.event.KeyEvent.VK_DELETE == evt.getKeyCode())
        {
            remove_selected_node();
        }
    }
    
    private void remove_selected_node()
    {
        int[]   row_arr = this.node_info_table.getSelectedRows();

        if (row_arr.length > 0)
        {
            for (int row : row_arr)
            {
                remove_selected_node(row);
            }
            this.node_info_table.updateUI();
        }
    }
    
    /**
     * remove the client info from the node table in the specified row.
     * @param row 
     */
    private void remove_selected_node(int row)
    {
        SocketAddress remote_address = (SocketAddress)this.client_table_model.getSocketAddress(row);
        this.server_socket.close_socket(remote_address);
        this.client_table_model.removeRow(row);
    }

    /**
     * Remove the client who is inactive for the longest time.
     */
    private void remove_inactive_client()
    {
        int     inactive_row = 0;
        Date    inactive_time = (Date)this.client_table_model.getAccessTime(0);
        Date    access_time;
        
        for (int i = 0; i < this.client_table_model.getRowCount(); i++)
        {
            access_time = (Date)this.client_table_model.getAccessTime(i);
            if (access_time.before(inactive_time))
            {
                inactive_time   = access_time;
                inactive_row    = i;
            }
        }
        
        remove_selected_node(inactive_row);
    }
    
    private boolean remove_client_info(SocketAddress remote_address)
    {
        boolean ret_val = false;
        
        for (int i = 0; i < this.client_table_model.getRowCount(); i++)
        {
            // Find the IP address in the table, and then remove it from the table.
            if (this.client_table_model.getSocketAddress(i).equals(remote_address))
            {
                // We find the address and port in the table, so we remove it
                this.client_table_model.removeRow(i);

                ret_val = true;
                break;
            }
        }
        
        return ret_val;
    }
    
    private void add_client_info(SocketAddress remote_address)
    {
        int session_id = (int)(Math.random() * 999999999);
        while (this.client_table_model.getRowCount() >= SimpleServerR.int_val.MAX_CLIENT_NUM)
        {
            remove_inactive_client();
        }

        // Add the address of the new one in the end of the table.
        this.client_table_model.addRow(new Object[]{
            String.format("%09d", session_id), remote_address, new Date()});
    }
    
    private int get_row_by_address(SocketAddress remote_address)
    {
        int ret_val = -1;
        
        for (int i = 0; i < this.client_table_model.getRowCount(); i++)
        {
            // Find the IP address in the table, and then remove it from the table.
            if (this.client_table_model.getSocketAddress(i).equals(remote_address))
            {
                ret_val = i;
                break;
            }
        }
        
        return ret_val;
    }
    
    private void update_access_time(int row)
    {
        this.client_table_model.setAccessTime(row, new Date());

        this.node_info_table.updateUI();
    }
         
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton bt_send;
    private javax.swing.JButton bt_delete;
    private javax.swing.JButton bt_start;
    private javax.swing.JButton bt_stop;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JLabel labelPort;
    private javax.swing.JLabel labelServer;
    private javax.swing.JLabel lb_dest_addr;
    private javax.swing.JLabel lb_input;
    private javax.swing.JLabel lb_output;
    private javax.swing.JLabel lb_source_addr;
    private javax.swing.JTable node_info_table;
    private javax.swing.JComboBox<String> server_type_list;
    private javax.swing.JTextField tf_port;
    private javax.swing.JTextArea  ta_recv;
    private javax.swing.JTextArea  ta_send;
    // End of variables declaration//GEN-END:variables
    
    private javax.swing.JScrollPane jscp_recv;
    private javax.swing.JScrollPane jscp_send;

    private SessionServer           server_socket;
    private boolean                 b_ui_occupied;  /*Is the UI occupied by some socket*/
    private ServerTableModel        client_table_model;
    
    private javax.swing.JCheckBox   hex_send;
    private javax.swing.JCheckBox   hex_recv;
    
    /**
     * The main version for each releasing.
     */
    private final String main_version = "V1.0";
    
    /**
     * The build version for building each time.
     */
    private final String build_version = ".01";
}
